You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
664 lines
21 KiB
664 lines
21 KiB
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
hasQuestionAnswerValue,
|
|
QuestionAnswersProvider,
|
|
useQuestionAnswers,
|
|
} from "@/components/questions/question-answer-storage";
|
|
import QuestionButton from "@/components/questions/question-button";
|
|
import { QuestionCheckbox } from "@/components/questions/question-checkbox";
|
|
import QuestionDate from "@/components/questions/question-date";
|
|
import QuestionDropdown from "@/components/questions/question-dropdown";
|
|
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
|
|
import QuestionFile from "@/components/questions/question-file";
|
|
import QuestionNumber from "@/components/questions/question-number";
|
|
import QuestionPhone from "@/components/questions/question-phone";
|
|
import QuestionPhoto from "@/components/questions/question-photo";
|
|
import QuestionRadio from "@/components/questions/question-radio";
|
|
import QuestionSectionFlow from "@/components/questions/question-section-flow";
|
|
import QuestionSlider from "@/components/questions/question-slider";
|
|
import QuestionText from "@/components/questions/question-text";
|
|
import NavigationButton from "@/components/ui/navigation-button";
|
|
import StickyHeader from "@/components/ui/sticky-header";
|
|
import { PageBackground } from "@/components/utils/page-background";
|
|
import {
|
|
getQuestionListItemBySlug,
|
|
isQuestionListItemVisibleForProfile,
|
|
isQuestionRequiredForProfile,
|
|
isQuestionVisibleForProfile,
|
|
type QuestionField,
|
|
} from "@/data/question-data";
|
|
import type { MarriageGender } from "@/hooks/marriage/types";
|
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
|
|
import { defaultLocale, type Locale } from "@/i18n/config";
|
|
import { useI18n } from "@/i18n/provider";
|
|
import TestIntroPage from "@/components/questions/test-intro-page";
|
|
import TestQuestionsFlow, { type TestQuestion } from "@/components/questions/test-questions-flow";
|
|
import AnswerPaceSheet from "./answer-pace-sheet";
|
|
|
|
type QuestionDetailClientProps = {
|
|
closeLabel: string;
|
|
continueLabel: string;
|
|
description: string;
|
|
informationLabel: string;
|
|
itemSlug: string;
|
|
locale?: Locale;
|
|
questionsListHref: string;
|
|
title: string;
|
|
};
|
|
|
|
type StoredQuestionField = {
|
|
label?: string;
|
|
value?: unknown;
|
|
};
|
|
|
|
type StoredAnswers = {
|
|
fields?: StoredQuestionField[];
|
|
};
|
|
|
|
function getQuestionStorageKey(slug: string) {
|
|
return `marriage:sections:${slug}:answers`;
|
|
}
|
|
|
|
function parseStoredAge(value: unknown) {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
const trimmedValue = value.trim();
|
|
|
|
if (!trimmedValue) {
|
|
return null;
|
|
}
|
|
|
|
const numericAge = Number(trimmedValue);
|
|
|
|
if (Number.isFinite(numericAge)) {
|
|
return numericAge;
|
|
}
|
|
|
|
const dateOfBirth = new Date(trimmedValue);
|
|
|
|
if (Number.isNaN(dateOfBirth.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
const today = new Date();
|
|
let age = today.getFullYear() - dateOfBirth.getFullYear();
|
|
const hasBirthdayPassed =
|
|
today.getMonth() > dateOfBirth.getMonth() ||
|
|
(today.getMonth() === dateOfBirth.getMonth() &&
|
|
today.getDate() >= dateOfBirth.getDate());
|
|
|
|
if (!hasBirthdayPassed) {
|
|
age -= 1;
|
|
}
|
|
|
|
return age >= 0 ? age : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getStoredAge() {
|
|
try {
|
|
const rawValue = window.localStorage.getItem(
|
|
getQuestionStorageKey("personal_info"),
|
|
);
|
|
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
|
|
const storedAnswers = JSON.parse(rawValue) as StoredAnswers;
|
|
const ageField = storedAnswers.fields?.find(
|
|
(field) => field.label === "Age",
|
|
);
|
|
|
|
if (ageField) {
|
|
return parseStoredAge(ageField.value);
|
|
}
|
|
|
|
const dateOfBirthField = storedAnswers.fields?.find(
|
|
(field) => field.label === "Date of Birth",
|
|
);
|
|
|
|
return parseStoredAge(dateOfBirthField?.value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function renderQuestion(
|
|
question: QuestionField,
|
|
questionIndex: number,
|
|
disabled?: boolean,
|
|
dobQuestion?: QuestionField,
|
|
dobQuestionIndex?: number,
|
|
) {
|
|
const compactTextHeight =
|
|
question.type === "text" &&
|
|
(question.title.toLowerCase().includes("name") ||
|
|
question.title.toLowerCase().includes("email") ||
|
|
question.title.toLowerCase().includes("city") ||
|
|
question.title.toLowerCase().includes("residence") ||
|
|
question.title.toLowerCase().includes("location") ||
|
|
question.title.toLowerCase().includes("description") ||
|
|
question.title.toLowerCase().includes("short") ||
|
|
question.title.toLowerCase().includes("duration") ||
|
|
question.title.toLowerCase().includes("reason") ||
|
|
question.title.toLowerCase().includes("lifestyle") ||
|
|
question.title.toLowerCase().includes("marja") ||
|
|
question.title.toLowerCase().includes("range") ||
|
|
question.title.toLowerCase().includes("ethnicity") ||
|
|
question.title.toLowerCase().includes("nationality") ||
|
|
question.title.toLowerCase().includes("مدت") ||
|
|
question.title.toLowerCase().includes("علت") ||
|
|
question.title.toLowerCase().includes("بازه") ||
|
|
question.title.toLowerCase().includes("قومیت") ||
|
|
question.title.toLowerCase().includes("ملیت") ||
|
|
question.title.toLowerCase().includes("کوتاه") ||
|
|
question.title.toLowerCase().includes("سبک زندگی") ||
|
|
question.title.toLowerCase().includes("مرجع")) &&
|
|
!question.title.toLowerCase().includes("detailed") &&
|
|
!question.title.toLowerCase().includes("biography")
|
|
? "h-[54px] min-h-0 py-2"
|
|
: undefined;
|
|
|
|
switch (question.type) {
|
|
case "button":
|
|
return (
|
|
<QuestionButton
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "checkbox":
|
|
return (
|
|
<QuestionCheckbox
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "date":
|
|
return (
|
|
<QuestionDate
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "dropdown":
|
|
return (
|
|
<QuestionDropdown
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "file":
|
|
return (
|
|
<QuestionFile
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "number":
|
|
if (
|
|
question.title === "Age" &&
|
|
dobQuestion &&
|
|
dobQuestionIndex !== undefined
|
|
) {
|
|
return (
|
|
<QuestionNumber
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
derivedFromQuestion={dobQuestion}
|
|
derivedFromQuestionIndex={dobQuestionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<QuestionNumber
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "phone":
|
|
return (
|
|
<QuestionPhone
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "photo":
|
|
return (
|
|
<QuestionPhoto
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "radio":
|
|
return (
|
|
<QuestionRadio
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "slider":
|
|
return (
|
|
<QuestionSlider
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
case "text":
|
|
return (
|
|
<QuestionText
|
|
question={question}
|
|
questionIndex={questionIndex}
|
|
heightClassName={compactTextHeight}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function QuestionFlowWrapper({
|
|
visibleQuestions,
|
|
itemSlug,
|
|
dobQuestion,
|
|
dobQuestionIndex,
|
|
requiredQuestionsCount,
|
|
continueLabel,
|
|
questionsListHref,
|
|
}: {
|
|
visibleQuestions: QuestionField[];
|
|
itemSlug: string;
|
|
dobQuestion?: QuestionField;
|
|
dobQuestionIndex?: number;
|
|
requiredQuestionsCount: number;
|
|
continueLabel: string;
|
|
questionsListHref: string;
|
|
}) {
|
|
const { getAnswerValue } = useQuestionAnswers();
|
|
|
|
return (
|
|
<QuestionSectionFlow
|
|
key={itemSlug}
|
|
total={requiredQuestionsCount}
|
|
continueLabel={continueLabel}
|
|
exitHref={questionsListHref}
|
|
optionalQuestionIndexes={visibleQuestions.flatMap((question, index) =>
|
|
question.required ? [] : [index],
|
|
)}
|
|
>
|
|
{visibleQuestions.map((question, questionIndex) => {
|
|
let isDisabled = false;
|
|
|
|
if (question.logic?.dependsOn) {
|
|
const { title, values } = question.logic.dependsOn;
|
|
const dependentQuestionIndex = visibleQuestions.findIndex(
|
|
(q) => q.title === title,
|
|
);
|
|
|
|
if (dependentQuestionIndex !== -1) {
|
|
const answer = getAnswerValue(
|
|
visibleQuestions[dependentQuestionIndex],
|
|
dependentQuestionIndex,
|
|
);
|
|
isDisabled = !values.includes(String(answer));
|
|
}
|
|
}
|
|
|
|
const answer = getAnswerValue(question, questionIndex);
|
|
const hasAnswer = hasQuestionAnswerValue(answer ?? null);
|
|
let isAnswered = hasAnswer;
|
|
|
|
if (hasAnswer) {
|
|
const isEmailQuestion =
|
|
question.title.toLowerCase().includes("email") ||
|
|
question.title.includes("ایمیل");
|
|
if (isEmailQuestion) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
isAnswered = emailRegex.test(String(answer).trim());
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={`${itemSlug}-${question.title}`}
|
|
data-question-required={String(question.required)}
|
|
data-question-optional={String(!question.required)}
|
|
data-question-index={questionIndex}
|
|
data-question-disabled={String(isDisabled)}
|
|
data-question-answered={String(isAnswered)}
|
|
>
|
|
{renderQuestion(
|
|
question,
|
|
questionIndex,
|
|
isDisabled,
|
|
dobQuestion,
|
|
dobQuestionIndex,
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</QuestionSectionFlow>
|
|
);
|
|
}
|
|
|
|
export default function QuestionDetailClient({
|
|
closeLabel,
|
|
continueLabel,
|
|
description,
|
|
informationLabel,
|
|
itemSlug,
|
|
locale = defaultLocale,
|
|
questionsListHref,
|
|
title,
|
|
}: QuestionDetailClientProps) {
|
|
const router = useRouter();
|
|
const { dictionary: t } = useI18n();
|
|
const [isTestStarted, setIsTestStarted] = useState(false);
|
|
const { data: profile, isLoading: isProfileLoading } = useMarriageProfileQuery();
|
|
const profileGender = profile?.gender;
|
|
const age = getStoredAge();
|
|
const item = getQuestionListItemBySlug(itemSlug, locale);
|
|
const profileContext = useMemo(
|
|
() => ({
|
|
age,
|
|
gender: profileGender as MarriageGender | null | undefined,
|
|
}),
|
|
[age, profileGender],
|
|
);
|
|
|
|
const visibleQuestions = useMemo(() => {
|
|
if (!item) {
|
|
return [];
|
|
}
|
|
|
|
const hasDobQuestion = item.questions.some(
|
|
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
|
|
);
|
|
|
|
return item.questions
|
|
.filter((question) => {
|
|
if (
|
|
hasDobQuestion &&
|
|
(question.title === "Age" || question.title === "سن")
|
|
) {
|
|
return false;
|
|
}
|
|
return isQuestionVisibleForProfile(question, profileContext);
|
|
})
|
|
.map((question) => ({
|
|
...question,
|
|
required: isQuestionRequiredForProfile(question, profileContext),
|
|
}));
|
|
}, [item, profileContext]);
|
|
|
|
const requiredQuestionsCount = useMemo(
|
|
() => visibleQuestions.filter((q) => q.required).length,
|
|
[visibleQuestions],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isProfileLoading) {
|
|
return;
|
|
}
|
|
|
|
if (!item || isQuestionListItemVisibleForProfile(item, profileContext)) {
|
|
return;
|
|
}
|
|
|
|
router.replace(questionsListHref);
|
|
}, [isProfileLoading, item, profileContext, questionsListHref, router]);
|
|
|
|
if (isProfileLoading && item) {
|
|
// Show component skeleton/layout structure while loading profile
|
|
} else if (!item || !isQuestionListItemVisibleForProfile(item, profileContext)) {
|
|
return null;
|
|
}
|
|
if (item && item.questions.length === 0) {
|
|
if (isTestStarted) {
|
|
const testQuestions: TestQuestion[] =
|
|
item.slug === "glasser_5_needs_test"
|
|
? [
|
|
{
|
|
id: 1,
|
|
text: "How important is financial security and long-term stability in your life?",
|
|
options: [
|
|
{ label: "Very Low", value: 1 },
|
|
{ label: "Low", value: 2 },
|
|
{ label: "Moderate", value: 3 },
|
|
{ label: "High", value: 4 },
|
|
{ label: "Very High", value: 5 },
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
text: "To what extent do you value personal freedom and flexibility in daily choices?",
|
|
options: [
|
|
{ label: "Very Low", value: 1 },
|
|
{ label: "Low", value: 2 },
|
|
{ label: "Moderate", value: 3 },
|
|
{ label: "High", value: 4 },
|
|
{ label: "Very High", value: 5 },
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
text: "How much do you seek recognition and personal achievement in your career/life?",
|
|
options: [
|
|
{ label: "Very Low", value: 1 },
|
|
{ label: "Low", value: 2 },
|
|
{ label: "Moderate", value: 3 },
|
|
{ label: "High", value: 4 },
|
|
{ label: "Very High", value: 5 },
|
|
],
|
|
},
|
|
{
|
|
id: 4,
|
|
text: "How essential is fun, humor, and playfulness in your relationship?",
|
|
options: [
|
|
{ label: "Very Low", value: 1 },
|
|
{ label: "Low", value: 2 },
|
|
{ label: "Moderate", value: 3 },
|
|
{ label: "High", value: 4 },
|
|
{ label: "Very High", value: 5 },
|
|
],
|
|
},
|
|
{
|
|
id: 5,
|
|
text: "How deep is your need for emotional connection and belonging with a partner?",
|
|
options: [
|
|
{ label: "Very Low", value: 1 },
|
|
{ label: "Low", value: 2 },
|
|
{ label: "Moderate", value: 3 },
|
|
{ label: "High", value: 4 },
|
|
{ label: "Very High", value: 5 },
|
|
],
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
id: 1,
|
|
text: "How many hours do you typically spend on social media each day?",
|
|
options: [
|
|
{ label: "Between 11 and 15 Years", value: "11_15" },
|
|
{ label: "Between 16 and 25 Years", value: "16_25" },
|
|
{ label: "Between 26 and 40 Years", value: "26_40" },
|
|
{ label: "Over 40 Years", value: "over_40" },
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
text: "I prefer spending my free time engaging in quiet, reflective activities.",
|
|
options: [
|
|
{ label: "Strongly Agree", value: "strongly_agree" },
|
|
{ label: "Agree", value: "agree" },
|
|
{ label: "Neutral / Uncertain", value: "neutral" },
|
|
{ label: "Disagree", value: "disagree" },
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
text: "When making important life decisions, I rely more on logical analysis than feelings.",
|
|
options: [
|
|
{ label: "Always Logical", value: "always_logical" },
|
|
{ label: "Mostly Logical", value: "mostly_logical" },
|
|
{ label: "Balanced", value: "balanced" },
|
|
{ label: "Mostly Intuitive", value: "mostly_intuitive" },
|
|
],
|
|
},
|
|
{
|
|
id: 4,
|
|
text: "How do you handle unexpected changes to your planned routine?",
|
|
options: [
|
|
{ label: "Adapt quickly and calmly", value: "adapt_quick" },
|
|
{ label: "Need a moment to adjust", value: "need_moment" },
|
|
{ label: "Feel stressed but manage", value: "stressed" },
|
|
{ label: "Prefer strict adherence to plans", value: "strict_plans" },
|
|
],
|
|
},
|
|
{
|
|
id: 5,
|
|
text: "In social gatherings, I usually initiate conversations with new acquaintances.",
|
|
options: [
|
|
{ label: "Very True", value: "very_true" },
|
|
{ label: "Somewhat True", value: "somewhat_true" },
|
|
{ label: "Rarely True", value: "rarely_true" },
|
|
{ label: "Not True At All", value: "not_true" },
|
|
],
|
|
},
|
|
];
|
|
|
|
return (
|
|
<TestQuestionsFlow
|
|
title={item.title}
|
|
questions={testQuestions}
|
|
closeLabel={closeLabel}
|
|
informationLabel={informationLabel}
|
|
onClose={() => setIsTestStarted(false)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const bulletKey = item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
|
|
const bullets = t.questions.testIntroBullets[bulletKey];
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader sticky={false} className="shrink-0">
|
|
<div className="flex items-center gap-4">
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<TestIntroPage
|
|
title={item.title}
|
|
estimateTime={item.estimate}
|
|
description={t.questions.testIntroEstimateLabel}
|
|
bulletPoints={bullets}
|
|
disclaimerText={t.questions.testIntroDisclaimer}
|
|
startLabel={t.questions.testIntroStart}
|
|
onStart={() => {
|
|
setIsTestStarted(true);
|
|
}}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
|
|
const dobQuestion = visibleQuestions.find(
|
|
(question) => question.title === "Date of Birth",
|
|
);
|
|
const dobQuestionIndex = visibleQuestions.findIndex(
|
|
(question) => question.title === "Date of Birth",
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<PageBackground disabled />
|
|
<AnswerPaceSheet
|
|
slug={item.slug}
|
|
title={title}
|
|
description={description}
|
|
continueLabel={continueLabel}
|
|
/>
|
|
|
|
<QuestionAnswersProvider slug={item.slug} questions={visibleQuestions}>
|
|
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
|
|
<StickyHeader sticky={false} className="shrink-0">
|
|
<div className="flex items-center gap-4">
|
|
<QuestionExitNavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="close"
|
|
iconLabel={closeLabel}
|
|
/>
|
|
<h1 className="min-w-0 flex-1 text-center font-semibold text-white truncate">
|
|
{item.title}
|
|
</h1>
|
|
<NavigationButton
|
|
className="shrink-0"
|
|
variant="transparent"
|
|
icon="info"
|
|
iconLabel={informationLabel}
|
|
/>
|
|
</div>
|
|
</StickyHeader>
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
|
|
<QuestionFlowWrapper
|
|
visibleQuestions={visibleQuestions}
|
|
itemSlug={item.slug}
|
|
dobQuestion={dobQuestion}
|
|
dobQuestionIndex={dobQuestionIndex}
|
|
requiredQuestionsCount={requiredQuestionsCount}
|
|
continueLabel={continueLabel}
|
|
questionsListHref={questionsListHref}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</QuestionAnswersProvider>
|
|
</>
|
|
);
|
|
}
|
|
|